{
 This file is part of Dev-C++
 Copyright (c) 2004 Bloodshed Software

 Dev-C++ is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or
 (at your option) any later version.

 Dev-C++ is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with Dev-C++; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
}

unit ViewToDoFrm;

interface

uses
    {$IFDEF WIN32}
    Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
    Dialogs, StdCtrls, ComCtrls, Menus;
{$ENDIF}
{$IFDEF LINUX}
SysUtils, Variants, Classes, QGraphics, QControls, QForms,
QDialogs, QStdCtrls, QComCtrls, QMenus;
{$ENDIF}

type
    PToDoRec = ^TToDoRec;
    TToDoRec = record
        TokenIndex: integer;
        Filename: AnsiString;
        Line: integer;
        ToLine: integer;
        User: AnsiString;
        Priority: integer;
        Description: AnsiString;
        IsDone: boolean;
    end;

    TViewToDoForm = class(TForm)
        lv: TListView;
        btnClose: TButton;
        chkNoDone: TCheckBox;
        cmbFilter: TComboBox;
        lblFilter: TLabel;
        procedure FormShow(Sender: TObject);
        procedure btnCloseClick(Sender: TObject);
        procedure FormCreate(Sender: TObject);
        procedure FormDestroy(Sender: TObject);
        procedure lvCustomDrawSubItem(Sender: TCustomListView; Item: TListItem; SubItem: Integer; State: TCustomDrawState; var DefaultDraw: Boolean);
        procedure FormClose(Sender: TObject; var Action: TCloseAction);
        procedure lvMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
        procedure lvCustomDrawItem(Sender: TCustomListView; Item: TListItem; State: TCustomDrawState; var DefaultDraw: Boolean);
        procedure lvColumnClick(Sender: TObject; Column: TListColumn);
        procedure lvCompare(Sender: TObject; Item1, Item2: TListItem; Data: Integer; var Compare: Integer);
        procedure lvDblClick(Sender: TObject);
        procedure chkNoDoneClick(Sender: TObject);
        procedure cmbFilterChange(Sender: TObject);
    private
        { Private declarations }
        fToDoList: TList;
        fSortColumn: integer;
        function MatchesMask(SearchStr, MaskStr: AnsiString): boolean;
        procedure LoadText;
        procedure BuildList;
        procedure AddFiles(Current, InProject, NotInProject, OpenOnly: boolean);
        procedure AddToDo(Filename: AnsiString);
        function BreakupToDo(Filename: AnsiString; sl: TStrings; Line: integer; Token: AnsiString; HasUser, HasPriority: boolean): integer;
    public
        { Public declarations }
    end;

    //var
    //	ViewToDoForm: TViewToDoForm;

implementation

uses main, editor, project, StrUtils, MultiLangSupport, devcfg;

{$R *.dfm}

function TViewToDoForm.BreakupToDo(Filename: AnsiString; sl: TStrings; Line: integer; Token: AnsiString; HasUser, HasPriority: boolean): integer;
var
    sUser: AnsiString;
    iPriority: integer;
    sDescription: AnsiString;
    Indent: integer;
    S: AnsiString;
    X, Y: integer;
    idx: integer;
    Done: boolean;
    MultiLine: boolean;
    td: PToDoRec;
    OrigLine: integer;
    TokenIndex: integer;
begin
    sUser := '';
    iPriority := 0;
    sDescription := '';

    OrigLine := Line;
    S := sl[Line];

    MultiLine := Pos('//', S) = 0;
    idx := Pos(Token, S);
    TokenIndex := idx;
    Inc(idx, 4); // skip "TODO"

    if HasUser or HasPriority then
        Inc(idx, 2); // skip " ("

    Delete(S, 1, idx - 1);
    if HasUser or HasPriority then begin
        idx := Pos('#', S);
        sUser := Copy(S, 1, idx - 1); // got user
        iPriority := StrToIntDef(S[idx + 1], 1); // got priority
    end;

    Indent := Pos(':', sl[Line]) + 1;
    idx := Pos(':', S);
    Delete(S, 1, idx + 1);
    Done := False;
    Y := Line;
    while (Y < sl.Count) and not Done do begin
        X := Indent;
        while (X <= Length(sl[Y])) and not Done do begin
            if (sl[Y][X] = '*') and (X < Length(sl[Y])) and (sl[Y][X + 1] = '/') then begin
                Done := True;
                Break;
            end;
            sDescription := sDescription + sl[Y][X];
            Inc(X);
        end;
        if not MultiLine then
            Break;
        if not Done then begin
            sDescription := sDescription + #13#10;
            Inc(Line);
        end;
        Inc(Y);
    end;

    td := New(PToDoRec);
    td^.TokenIndex := TokenIndex;
    td^.Filename := Filename;
    td^.Line := OrigLine;
    td^.ToLine := Line;
    td^.User := sUser;
    td^.Priority := iPriority;
    td^.Description := sDescription;
    td^.IsDone := CompareText(Token, 'TODO') <> 0;
    fToDoList.Add(td);

    Result := Line;
end;

procedure TViewToDoForm.AddToDo(Filename: AnsiString);
var
    sl: TStrings;
    I: integer;
begin
    sl := TStringList.Create;
    try
        for I := 0 to MainForm.PageControl.PageCount - 1 do
            if TEditor(MainForm.PageControl.Pages[I].Tag).FileName = Filename then
                sl.Assign(TEditor(MainForm.PageControl.Pages[I].Tag).Text.UnCollapsedLines)
            else if FileExists(Filename) then
                sl.LoadFromFile(Filename);
        if sl.Count = 0 then
            if FileExists(Filename) then
                sl.LoadFromFile(Filename);
        I := 0;
        while I < sl.Count do begin
            //		if MatchesMask(sl[I], '*/? TODO ([a-z0-9_]*#[1-9]#)*:*') then
            if MatchesMask(sl[I], '*/? TODO (?*#?#)*:*') then
                BreakupToDo(Filename, sl, I, 'TODO', True, True) // full info TODO
            else if MatchesMask(sl[I], '*/? DONE (?*#?#)*:*') then
                BreakupToDo(Filename, sl, I, 'DONE', True, True) // full info DONE
            else if MatchesMask(sl[I], '*/? TODO (#?#)*:*') then
                BreakupToDo(Filename, sl, I, 'TODO', False, True) // only priority info TODO
            else if MatchesMask(sl[I], '*/? DONE (#?#)*:*') then
                BreakupToDo(Filename, sl, I, 'DONE', False, True) // only priority info DONE
            else if MatchesMask(sl[I], '*/?*TODO*:*') then
                BreakupToDo(Filename, sl, I, 'TODO', False, False) // custom TODO
            else if MatchesMask(sl[I], '*/?*DONE*:*') then
                BreakupToDo(Filename, sl, I, 'DONE', False, False); // custom DONE
            Inc(I);
        end;
    finally
        sl.Free;
    end;
end;

procedure TViewToDoForm.AddFiles(Current, InProject, NotInProject, OpenOnly: boolean);
var
    e: TEditor;
    idx: integer;
begin
    if Current then begin
        e := MainForm.GetEditor;
        if Assigned(e) then
            AddToDo(e.FileName);
        Exit;
    end;

    if InProject and not OpenOnly then begin
        if Assigned(MainForm.fProject) then
            for idx := 0 to pred(MainForm.fProject.Units.Count) do
                AddToDo(MainForm.fProject.Units[idx].filename);
    end;

    if OpenOnly then begin
        for idx := 0 to pred(MainForm.PageControl.PageCount) do begin
            e := MainForm.GetEditor(idx);
            if Assigned(e) then
                if InProject and e.InProject then
                    AddToDo(e.FileName)
        end;
    end;

    if NotInProject then begin
        for idx := 0 to pred(MainForm.PageControl.PageCount) do begin
            e := MainForm.GetEditor(idx);
            if Assigned(e) then
                if not e.InProject then
                    AddToDo(e.FileName);
        end;
    end;
end;

procedure TViewToDoForm.FormShow(Sender: TObject);
begin
    cmbFilter.ItemIndex := 5;
    fToDoList.Clear;
    lv.Items.Clear;
    AddFiles(True, False, True, True); // default is current only
    BuildList;
end;

procedure TViewToDoForm.btnCloseClick(Sender: TObject);
begin
    Close;
end;

procedure TViewToDoForm.FormCreate(Sender: TObject);
begin
    fToDoList := TList.Create;
    fSortColumn := 0;
    LoadText;
end;

procedure TViewToDoForm.FormDestroy(Sender: TObject);
begin
    while fToDoList.Count > 0 do
        if Assigned(fToDoList[0]) then begin
            Dispose(PToDoRec(fToDoList[0]));
            fToDoList.Delete(0);
        end;
    fToDoList.Clear;
    fToDoList.Free;
end;

procedure TViewToDoForm.lvCustomDrawSubItem(Sender: TCustomListView;
    Item: TListItem; SubItem: Integer; State: TCustomDrawState;
    var DefaultDraw: Boolean);
begin
    if Assigned(Item.Data) then
        if PToDoRec(Item.Data)^.IsDone then
            Sender.Canvas.Font.Style := Sender.Canvas.Font.Style + [fsStrikeOut]
        else
            Sender.Canvas.Font.Style := Sender.Canvas.Font.Style - [fsStrikeOut];
    DefaultDraw := True;
end;

procedure TViewToDoForm.lvCustomDrawItem(Sender: TCustomListView;
    Item: TListItem; State: TCustomDrawState; var DefaultDraw: Boolean);
begin
    if Assigned(Item.Data) then
        Item.Checked := PToDoRec(Item.Data)^.IsDone;
    DefaultDraw := True;
end;

procedure TViewToDoForm.FormClose(Sender: TObject;
    var Action: TCloseAction);
begin
    Action := caFree;
end;

procedure TViewToDoForm.LoadText;
begin

    // Set interface font
    Font.Name := devData.InterfaceFont;
    Font.Size := devData.InterfaceFontSize;

    //    Caption := Lang[ID_VIEWTODO_MENUITEM];
    //
    //{$IFDEF WIN32}
    //    lv.Column[0].Caption := Lang[ID_VIEWTODO_DONE];
    //    lv.Column[1].Caption := Lang[ID_ADDTODO_PRIORITY];
    //    lv.Column[2].Caption := Lang[ID_ADDTODO_DESCRIPTION];
    //    lv.Column[3].Caption := Lang[ID_VIEWTODO_FILENAME];
    //    lv.Column[4].Caption := Lang[ID_ADDTODO_USER];
    //{$ENDIF}
    //{$IFDEF LINUX}
    //    lv.Columns[0].Caption := Lang[ID_VIEWTODO_DONE];
    //    lv.Columns[1].Caption := Lang[ID_ADDTODO_PRIORITY];
    //    lv.Columns[2].Caption := Lang[ID_ADDTODO_DESCRIPTION];
    //    lv.Columns[3].Caption := Lang[ID_VIEWTODO_FILENAME];
    //    lv.Columns[4].Caption := Lang[ID_ADDTODO_USER];
    //{$ENDIF}
    //
    //    chkNoDone.Caption := Lang[ID_VIEWTODO_NOSHOWDONE];
    //    btnClose.Caption := Lang[ID_BTN_CLOSE];
    //    lblFilter.Caption := Lang[ID_VIEWTODO_FILTER];
    //    cmbFilter.Items[0] := Lang[ID_VIEWTODO_FILTERONE];
    //    cmbFilter.Items[1] := Lang[ID_VIEWTODO_FILTERTWO];
    //    cmbFilter.Items[2] := Lang[ID_VIEWTODO_FILTERTHREE];
    //    cmbFilter.Items[3] := Lang[ID_VIEWTODO_FILTERFOUR];
    //    cmbFilter.Items[4] := Lang[ID_VIEWTODO_FILTERFIVE];
    //    cmbFilter.Items[5] := Lang[ID_VIEWTODO_FILTERSIX];
end;

procedure TViewToDoForm.lvMouseDown(Sender: TObject; Button: TMouseButton;
    Shift: TShiftState; X, Y: Integer);
var
    e: TEditor;
    Item: TListItem;
begin
    if not (htOnStateIcon in lv.GetHitTestInfoAt(X, Y)) then
        Exit;

    Item := lv.GetItemAt(X, Y);
    if not Assigned(Item) then
        Exit;
    if not Assigned(Item.Data) then
        Exit;

    e := MainForm.GetEditorFromFileName(PToDoRec(Item.Data)^.Filename);
    if Assigned(e) then begin
        PToDoRec(Item.Data)^.IsDone := Item.Checked;
        if Item.Checked then begin
            e.Text.UnCollapsedLines[PToDoRec(Item.Data)^.Line] := StringReplace(e.Text.UnCollapsedLines[PToDoRec(Item.Data)^.Line], 'TODO', 'DONE', []);
            if chkNoDone.Checked then
                BuildList;
        end
        else
            e.Text.UnCollapsedLines[PToDoRec(Item.Data)^.Line] := StringReplace(e.Text.UnCollapsedLines[PToDoRec(Item.Data)^.Line], 'DONE', 'TODO', []);
        e.Text.Modified := True;
        lv.Refresh;
    end;
end;

procedure TViewToDoForm.lvColumnClick(Sender: TObject;
    Column: TListColumn);
begin
    fSortColumn := Column.Index;
    TCustomListView(Sender).CustomSort(nil, 0);
end;

procedure TViewToDoForm.lvCompare(Sender: TObject; Item1, Item2: TListItem;
    Data: Integer; var Compare: Integer);
var
    idx: Integer;
begin
    if fSortColumn = 0 then begin
        if PToDoRec(Item1.Data)^.IsDone and not PToDoRec(Item2.Data)^.IsDone then
            Compare := 1
        else if not PToDoRec(Item1.Data)^.IsDone and PToDoRec(Item2.Data)^.IsDone then
            Compare := -1
        else
            Compare := 0;
    end
    else begin
        idx := fSortColumn - 1;
        Compare := CompareText(Item1.SubItems[idx], Item2.SubItems[idx]);
    end;
end;

procedure TViewToDoForm.lvDblClick(Sender: TObject);
var
    e: TEditor;
begin
    if not Assigned(lv.Selected) then
        Exit;
    if not Assigned(lv.Selected.Data) then
        Exit;

    e := MainForm.GetEditorFromFilename(PToDoRec(lv.Selected.Data)^.Filename);
    if Assigned(e) then begin
        e.SetCaretPos(PToDoRec(lv.Selected.Data)^.Line + 1, 1);
        Close;
    end;
end;

procedure TViewToDoForm.chkNoDoneClick(Sender: TObject);
begin
    BuildList;
end;

procedure TViewToDoForm.BuildList;
var
    I: integer;
    td: PToDoRec;
    S: AnsiString;
begin
    lv.Items.BeginUpdate;
    lv.Items.Clear;
    for I := 0 to fToDoList.Count - 1 do begin
        td := PToDoRec(fToDoList[I]);
        if (chkNoDone.Checked and not td^.IsDone) or not chkNoDone.Checked then
            with lv.Items.Add do begin
                Caption := '';
                SubItems.Add(IntToStr(td^.Priority));
                S := StringReplace(td^.Description, #13#10, ' ', [rfReplaceAll]);
                S := StringReplace(S, #9, ' ', [rfReplaceAll]);
                SubItems.Add(S);
                SubItems.Add(td^.Filename);
                SubItems.Add(td^.User);
                Data := td;
            end;
    end;
    lv.CustomSort(nil, 0);
    lv.Items.EndUpdate;
end;

function TViewToDoForm.MatchesMask(SearchStr, MaskStr: AnsiString): boolean;
var
    Matches: boolean;
    MaskIndex: integer;
    SearchIndex: integer;
    NextMatch: Char;
begin
    Matches := True;
    MaskIndex := 1;
    SearchIndex := 1;

    if (MaskStr = '') or (SearchStr = '') then
        Matches := False;

    if Pos('*?', MaskStr) > 0 then // illegal
        Matches := False;
    if Pos('**', MaskStr) > 0 then // illegal
        Matches := False;

    while Matches do begin
        case MaskStr[MaskIndex] of
            '*': begin
                    if MaskIndex < Length(MaskStr) then
                        NextMatch := MaskStr[MaskIndex + 1]
                    else
                        NextMatch := #0;
                    while SearchIndex <= Length(SearchStr) do begin
                        if SearchStr[SearchIndex] = NextMatch then begin
                            Inc(SearchIndex);
                            Inc(MaskIndex, 2);
                            Break;
                        end;
                        Inc(SearchIndex);
                    end;
                    if (SearchIndex = Length(SearchStr)) and (MaskIndex < Length(MaskStr)) then
                        Matches := False;
                end;
            '?': begin
                    Inc(SearchIndex);
                    Inc(MaskIndex);
                end;
        else
            if MaskStr[MaskIndex] <> SearchStr[SearchIndex] then
                Matches := False
            else begin
                Inc(MaskIndex);
                Inc(SearchIndex);
            end;
        end;

        if MaskIndex > Length(MaskStr) then
            Break;
        if SearchIndex > Length(SearchStr) then
            Break;
    end;
    if (MaskIndex = Length(MaskStr)) and (MaskStr[MaskIndex] = '*') then
        MaskIndex := Length(MaskStr) + 1;

    Result := Matches and (MaskIndex > Length(MaskStr)) and (SearchIndex > Length(SearchStr));
end;

procedure TViewToDoForm.cmbFilterChange(Sender: TObject);
begin
    {
     0 = All files (in project and not)
     1 = Open files only (in project and not)
     2 = All project files
     3 = Open project files only
     4 = Non-project open files
     5 = Current file only
    }
    fToDoList.Clear;
    lv.Items.Clear;
    case cmbFilter.ItemIndex of
        0: AddFiles(False, True, True, False);
        1: AddFiles(False, True, True, True);
        2: AddFiles(False, True, False, False);
        3: AddFiles(False, True, False, True);
        4: AddFiles(False, False, True, True);
        5: AddFiles(True, False, True, True);
    else
        AddFiles(True, False, True, True); // default is the same with 5
    end;
    BuildList;
end;

end.
